/**
 ******************************************************************************
 *
 * rwnx_cmds.c
 *
 * Handles queueing (push to IPC, ack/cfm from IPC) of commands issued to
 * LMAC FW
 *
 * Copyright (C) RivieraWaves 2014-2019
 *
 ******************************************************************************
 */

#include <linux/list.h>

#include "rwnx_cmds.h"
#include "rwnx_defs.h"
#include "rwnx_strs.h"
//#define CREATE_TRACE_POINTS
#include "rwnx_events.h"
#include "aicwf_txrxif.h"
#ifdef AICWF_SDIO_SUPPORT
#include "aicwf_sdio.h"
#else
#include "aicwf_usb.h"
#endif
/**
 *
 */
extern int aicwf_sdio_writeb(struct aic_sdio_dev *sdiodev, uint regaddr, u8 val);

static void cmd_dump(const struct rwnx_cmd *cmd)
{
	printk(KERN_CRIT "tkn[%d]  flags:%04x  result:%3d  cmd:%4d-%-24s - reqcfm(%4d-%-s)\n",
		   cmd->tkn, cmd->flags, cmd->result, cmd->id, RWNX_ID2STR(cmd->id),
		   cmd->reqid, cmd->reqid != (lmac_msg_id_t)-1 ? RWNX_ID2STR(cmd->reqid) : "none");
}

/**
 *
 */
static void cmd_complete(struct rwnx_cmd_mgr *cmd_mgr, struct rwnx_cmd *cmd)
{
	//RWNX_DBG(RWNX_FN_ENTRY_STR);
	lockdep_assert_held(&cmd_mgr->lock);

	list_del(&cmd->list);
	cmd_mgr->queue_sz--;

	cmd->flags |= RWNX_CMD_FLAG_DONE;
	if (cmd->flags & RWNX_CMD_FLAG_NONBLOCK) {
		kfree(cmd);
	} else {
		if (RWNX_CMD_WAIT_COMPLETE(cmd->flags)) {
			cmd->result = 0;
			complete(&cmd->complete);
		}
	}
}

int cmd_mgr_queue_force_defer(struct rwnx_cmd_mgr *cmd_mgr, struct rwnx_cmd *cmd)
{
	bool defer_push = false;

	RWNX_DBG(RWNX_FN_ENTRY_STR);
#ifdef CREATE_TRACE_POINTS
	trace_msg_send(cmd->id);
#endif
	spin_lock_bh(&cmd_mgr->lock);

	if (cmd_mgr->state == RWNX_CMD_MGR_STATE_CRASHED) {
		printk(KERN_CRIT"cmd queue crashed\n");
		cmd->result = -EPIPE;
		spin_unlock_bh(&cmd_mgr->lock);
		return -EPIPE;
	}

	#ifndef CONFIG_RWNX_FHOST
	if (!list_empty(&cmd_mgr->cmds)) {
		if (cmd_mgr->queue_sz == cmd_mgr->max_queue_sz) {
			printk(KERN_CRIT"Too many cmds (%d) already queued\n",
				   cmd_mgr->max_queue_sz);
			cmd->result = -ENOMEM;
			spin_unlock_bh(&cmd_mgr->lock);
			return -ENOMEM;
		}
	}
	#endif

	cmd->flags |= RWNX_CMD_FLAG_WAIT_PUSH;
	defer_push = true;

	if (cmd->flags & RWNX_CMD_FLAG_REQ_CFM)
		cmd->flags |= RWNX_CMD_FLAG_WAIT_CFM;

	cmd->tkn    = cmd_mgr->next_tkn++;
	cmd->result = -EINTR;

	if (!(cmd->flags & RWNX_CMD_FLAG_NONBLOCK))
		init_completion(&cmd->complete);

	list_add_tail(&cmd->list, &cmd_mgr->cmds);
	cmd_mgr->queue_sz++;
	spin_unlock_bh(&cmd_mgr->lock);

	WAKE_CMD_WORK(cmd_mgr);
	return 0;
}

static int cmd_mgr_queue(struct rwnx_cmd_mgr *cmd_mgr, struct rwnx_cmd *cmd)
{
#ifdef AICWF_SDIO_SUPPORT
	int ret;
	struct aic_sdio_dev *sdiodev = container_of(cmd_mgr, struct aic_sdio_dev, cmd_mgr);
#endif
#ifdef AICWF_USB_SUPPORT
	struct aic_usb_dev *usbdev = container_of(cmd_mgr, struct aic_usb_dev, cmd_mgr);
#endif
	bool defer_push = false;

	//RWNX_DBG(RWNX_FN_ENTRY_STR);
#ifdef CREATE_TRACE_POINTS
	trace_msg_send(cmd->id);
#endif
	spin_lock_bh(&cmd_mgr->lock);

	if (cmd_mgr->state == RWNX_CMD_MGR_STATE_CRASHED) {
		printk(KERN_CRIT"cmd queue crashed\n");
		cmd->result = -EPIPE;
		spin_unlock_bh(&cmd_mgr->lock);
		return -EPIPE;
	}

	#ifndef CONFIG_RWNX_FHOST
	if (!list_empty(&cmd_mgr->cmds)) {
		struct rwnx_cmd *last;

		if (cmd_mgr->queue_sz == cmd_mgr->max_queue_sz) {
			printk(KERN_CRIT"Too many cmds (%d) already queued\n",
				   cmd_mgr->max_queue_sz);
			cmd->result = -ENOMEM;
			spin_unlock_bh(&cmd_mgr->lock);
			return -ENOMEM;
		}
		last = list_entry(cmd_mgr->cmds.prev, struct rwnx_cmd, list);
		if (last->flags & (RWNX_CMD_FLAG_WAIT_ACK | RWNX_CMD_FLAG_WAIT_PUSH | RWNX_CMD_FLAG_WAIT_CFM)) {
#if 0 // queue even NONBLOCK command.
			if (cmd->flags & RWNX_CMD_FLAG_NONBLOCK) {
				printk(KERN_CRIT"cmd queue busy\n");
				cmd->result = -EBUSY;
				spin_unlock_bh(&cmd_mgr->lock);
				return -EBUSY;
			}
#endif
			cmd->flags |= RWNX_CMD_FLAG_WAIT_PUSH;
			defer_push = true;
		}
	}
	#endif

#if 0
	cmd->flags |= RWNX_CMD_FLAG_WAIT_ACK;
#endif
	if (cmd->flags & RWNX_CMD_FLAG_REQ_CFM)
		cmd->flags |= RWNX_CMD_FLAG_WAIT_CFM;

	cmd->tkn    = cmd_mgr->next_tkn++;
	cmd->result = -EINTR;

	if (!(cmd->flags & RWNX_CMD_FLAG_NONBLOCK))
		init_completion(&cmd->complete);

	list_add_tail(&cmd->list, &cmd_mgr->cmds);
	cmd_mgr->queue_sz++;

	if (cmd->a2e_msg->id == ME_TRAFFIC_IND_REQ
	#ifdef AICWF_ARP_OFFLOAD
		|| cmd->a2e_msg->id == MM_SET_ARPOFFLOAD_REQ
	#endif
	) {
		defer_push = true;
		cmd->flags |= RWNX_CMD_FLAG_WAIT_PUSH;
		//printk("defer push: tkn=%d\r\n", cmd->tkn);
	}

	spin_unlock_bh(&cmd_mgr->lock);
	if (!defer_push) {
		//printk("queue:id=%x, param_len=%u\n",cmd->a2e_msg->id, cmd->a2e_msg->param_len);
		#ifdef AICWF_SDIO_SUPPORT
		aicwf_set_cmd_tx((void *)(sdiodev), cmd->a2e_msg, sizeof(struct lmac_msg) + cmd->a2e_msg->param_len);
		#else
		aicwf_set_cmd_tx((void *)(usbdev), cmd->a2e_msg, sizeof(struct lmac_msg) + cmd->a2e_msg->param_len);
		#endif
		//rwnx_ipc_msg_push(rwnx_hw, cmd, RWNX_CMD_A2EMSG_LEN(cmd->a2e_msg));

		kfree(cmd->a2e_msg);
	} else {
		WAKE_CMD_WORK(cmd_mgr);
		return 0;
	}

	if (!(cmd->flags & RWNX_CMD_FLAG_NONBLOCK)) {
		#ifdef CONFIG_RWNX_FHOST
		if (wait_for_completion_killable(&cmd->complete)) {
			cmd->result = -EINTR;
			spin_lock_bh(&cmd_mgr->lock);
			cmd_complete(cmd_mgr, cmd);
			spin_unlock_bh(&cmd_mgr->lock);
			/* TODO: kill the cmd at fw level */
		}
		#else
		unsigned long tout = msecs_to_jiffies(RWNX_80211_CMD_TIMEOUT_MS * cmd_mgr->queue_sz);
		if (!wait_for_completion_timeout(&cmd->complete, tout)) {
			printk(KERN_CRIT"cmd timed-out\n");
			#ifdef AICWF_SDIO_SUPPORT
			ret = aicwf_sdio_writeb(sdiodev, SDIOWIFI_WAKEUP_REG, 2);
			if (ret < 0) {
				sdio_err("reg:%d write failed!\n", SDIOWIFI_WAKEUP_REG);
			}
		#endif

			cmd_dump(cmd);
			spin_lock_bh(&cmd_mgr->lock);
			cmd_mgr->state = RWNX_CMD_MGR_STATE_CRASHED;
			if (!(cmd->flags & RWNX_CMD_FLAG_DONE)) {
				cmd->result = -ETIMEDOUT;
				cmd_complete(cmd_mgr, cmd);
			}
			spin_unlock_bh(&cmd_mgr->lock);
		} else {
			kfree(cmd);
			if (!list_empty(&cmd_mgr->cmds))
				WAKE_CMD_WORK(cmd_mgr);
		}
		#endif
	} else {
		cmd->result = 0;
	}

	return 0;
}

/**
 *
 */
static int cmd_mgr_llind(struct rwnx_cmd_mgr *cmd_mgr, struct rwnx_cmd *cmd)
{
	struct rwnx_cmd *cur, *acked = NULL, *next = NULL;

	RWNX_DBG(RWNX_FN_ENTRY_STR);

	spin_lock_bh(&cmd_mgr->lock);
	list_for_each_entry(cur, &cmd_mgr->cmds, list) {
		if (!acked) {
			if (cur->tkn == cmd->tkn) {
				if (WARN_ON_ONCE(cur != cmd)) {
					cmd_dump(cmd);
				}
				acked = cur;
				continue;
			}
		}
		if (cur->flags & RWNX_CMD_FLAG_WAIT_PUSH) {
				next = cur;
				break;
		}
	}
	if (!acked) {
		printk(KERN_CRIT "Error: acked cmd not found\n");
	} else {
		cmd->flags &= ~RWNX_CMD_FLAG_WAIT_ACK;
		if (RWNX_CMD_WAIT_COMPLETE(cmd->flags))
			cmd_complete(cmd_mgr, cmd);
	}

	if (next) {
	#if 0 //there is no ack
		struct rwnx_hw *rwnx_hw = container_of(cmd_mgr, struct rwnx_hw, cmd_mgr);
		next->flags &= ~RWNX_CMD_FLAG_WAIT_PUSH;
		rwnx_ipc_msg_push(rwnx_hw, next, RWNX_CMD_A2EMSG_LEN(next->a2e_msg));
		kfree(next->a2e_msg);
	#endif
	}
	spin_unlock(&cmd_mgr->lock);

	return 0;
}

void cmd_mgr_task_process(struct work_struct *work)
{
	struct rwnx_cmd_mgr *cmd_mgr = container_of(work, struct rwnx_cmd_mgr, cmdWork);
	struct rwnx_cmd *cur, *next = NULL;
	unsigned long tout;

	RWNX_DBG(RWNX_FN_ENTRY_STR);

	while (1) {
		next = NULL;
		spin_lock_bh(&cmd_mgr->lock);

		list_for_each_entry(cur, &cmd_mgr->cmds, list) {
			if (cur->flags & RWNX_CMD_FLAG_WAIT_PUSH) { //just judge the first
					next = cur;
			}
			break;
		}
		spin_unlock_bh(&cmd_mgr->lock);

		if (next == NULL)
			break;

		if (next) {
#ifdef AICWF_SDIO_SUPPORT
			struct aic_sdio_dev *sdiodev = container_of(cmd_mgr, struct aic_sdio_dev, cmd_mgr);
#endif
#ifdef AICWF_USB_SUPPORT
			struct aic_usb_dev *usbdev = container_of(cmd_mgr, struct aic_usb_dev, cmd_mgr);
#endif
			next->flags &= ~RWNX_CMD_FLAG_WAIT_PUSH;

			//printk("cmd_process, cmd->id=%d, tkn=%d\r\n",next->reqid, next->tkn);
			//rwnx_ipc_msg_push(rwnx_hw, next, RWNX_CMD_A2EMSG_LEN(next->a2e_msg));
#ifdef AICWF_SDIO_SUPPORT
			aicwf_set_cmd_tx((void *)(sdiodev), next->a2e_msg, sizeof(struct lmac_msg) + next->a2e_msg->param_len);
#else
			aicwf_set_cmd_tx((void *)(usbdev), next->a2e_msg, sizeof(struct lmac_msg) + next->a2e_msg->param_len);
#endif
			kfree(next->a2e_msg);

			tout = msecs_to_jiffies(RWNX_80211_CMD_TIMEOUT_MS * cmd_mgr->queue_sz);
			if (!wait_for_completion_timeout(&next->complete, tout)) {
				printk(KERN_CRIT"cmd timed-out\n");
#ifdef AICWF_SDIO_SUPPORT
				if (aicwf_sdio_writeb(sdiodev, SDIOWIFI_WAKEUP_REG, 2) < 0) {
					sdio_err("reg:%d write failed!\n", SDIOWIFI_WAKEUP_REG);
				}
#endif
				cmd_dump(next);
				spin_lock_bh(&cmd_mgr->lock);
				cmd_mgr->state = RWNX_CMD_MGR_STATE_CRASHED;
				if (!(next->flags & RWNX_CMD_FLAG_DONE)) {
					next->result = -ETIMEDOUT;
					cmd_complete(cmd_mgr, next);
				}
				spin_unlock_bh(&cmd_mgr->lock);
			} else
				kfree(next);
		}
	}

}


static int cmd_mgr_run_callback(struct rwnx_hw *rwnx_hw, struct rwnx_cmd *cmd,
								struct rwnx_cmd_e2amsg *msg, msg_cb_fct cb)
{
	int res;

	if (!cb) {
		return 0;
	}
	//RWNX_DBG(RWNX_FN_ENTRY_STR);
	spin_lock_bh(&rwnx_hw->cb_lock);
	res = cb(rwnx_hw, cmd, msg);
	spin_unlock_bh(&rwnx_hw->cb_lock);

	return res;
}

/**
 *

 */
static int cmd_mgr_msgind(struct rwnx_cmd_mgr *cmd_mgr, struct rwnx_cmd_e2amsg *msg,
						  msg_cb_fct cb)
{
#ifdef AICWF_SDIO_SUPPORT
	struct aic_sdio_dev *sdiodev = container_of(cmd_mgr, struct aic_sdio_dev, cmd_mgr);
	struct rwnx_hw *rwnx_hw = sdiodev->rwnx_hw;
#endif
#ifdef AICWF_USB_SUPPORT
	struct aic_usb_dev *usbdev = container_of(cmd_mgr, struct aic_usb_dev, cmd_mgr);
	struct rwnx_hw *rwnx_hw = usbdev->rwnx_hw;
#endif
	struct rwnx_cmd *cmd;
	bool found = false;

	// RWNX_DBG(RWNX_FN_ENTRY_STR);
#ifdef CREATE_TRACE_POINTS
	trace_msg_recv(msg->id);
#endif
	//printk("cmd->id=%x\n", msg->id);
	spin_lock_bh(&cmd_mgr->lock);
	list_for_each_entry(cmd, &cmd_mgr->cmds, list) {
		if (cmd->reqid == msg->id &&
			(cmd->flags & RWNX_CMD_FLAG_WAIT_CFM)) {

			if (!cmd_mgr_run_callback(rwnx_hw, cmd, msg, cb)) {
				found = true;
				cmd->flags &= ~RWNX_CMD_FLAG_WAIT_CFM;

				if (WARN((msg->param_len > RWNX_CMD_E2AMSG_LEN_MAX),
						 "Unexpect E2A msg len %d > %d\n", msg->param_len,
						 RWNX_CMD_E2AMSG_LEN_MAX)) {
					msg->param_len = RWNX_CMD_E2AMSG_LEN_MAX;
				}

				if (cmd->e2a_msg && msg->param_len)
					memcpy(cmd->e2a_msg, &msg->param, msg->param_len);

				if (RWNX_CMD_WAIT_COMPLETE(cmd->flags))
					cmd_complete(cmd_mgr, cmd);

				break;
			}
		}
	}
	spin_unlock_bh(&cmd_mgr->lock);

	if (!found)
		cmd_mgr_run_callback(rwnx_hw, NULL, msg, cb);

	return 0;
}

/**
 *
 */
static void cmd_mgr_print(struct rwnx_cmd_mgr *cmd_mgr)
{
	struct rwnx_cmd *cur;

	spin_lock_bh(&cmd_mgr->lock);
	RWNX_DBG("q_sz/max: %2d / %2d - next tkn: %d\n",
			 cmd_mgr->queue_sz, cmd_mgr->max_queue_sz,
			 cmd_mgr->next_tkn);
	list_for_each_entry(cur, &cmd_mgr->cmds, list) {
		cmd_dump(cur);
	}
	spin_unlock_bh(&cmd_mgr->lock);
}

static void cmd_mgr_drain(struct rwnx_cmd_mgr *cmd_mgr)
{
	struct rwnx_cmd *cur, *nxt;

	RWNX_DBG(RWNX_FN_ENTRY_STR);

	spin_lock_bh(&cmd_mgr->lock);
	list_for_each_entry_safe(cur, nxt, &cmd_mgr->cmds, list) {
		list_del(&cur->list);
		cmd_mgr->queue_sz--;
		if (!(cur->flags & RWNX_CMD_FLAG_NONBLOCK))
			complete(&cur->complete);
	}
	spin_unlock_bh(&cmd_mgr->lock);
}

void rwnx_cmd_mgr_init(struct rwnx_cmd_mgr *cmd_mgr)
{
	RWNX_DBG(RWNX_FN_ENTRY_STR);

	INIT_LIST_HEAD(&cmd_mgr->cmds);
	cmd_mgr->state = RWNX_CMD_MGR_STATE_INITED;
	spin_lock_init(&cmd_mgr->lock);
	cmd_mgr->max_queue_sz = RWNX_CMD_MAX_QUEUED;
	cmd_mgr->queue  = &cmd_mgr_queue;
	cmd_mgr->print  = &cmd_mgr_print;
	cmd_mgr->drain  = &cmd_mgr_drain;
	cmd_mgr->llind  = &cmd_mgr_llind;
	cmd_mgr->msgind = &cmd_mgr_msgind;

	INIT_WORK(&cmd_mgr->cmdWork, cmd_mgr_task_process);
	cmd_mgr->cmd_wq = create_singlethread_workqueue("cmd_wq");
	if (!cmd_mgr->cmd_wq) {
		txrx_err("insufficient memory to create cmd workqueue.\n");
		return;
	}
}

void rwnx_cmd_mgr_deinit(struct rwnx_cmd_mgr *cmd_mgr)
{
	cmd_mgr->print(cmd_mgr);
	cmd_mgr->drain(cmd_mgr);
	cmd_mgr->print(cmd_mgr);
	flush_workqueue(cmd_mgr->cmd_wq);
	destroy_workqueue(cmd_mgr->cmd_wq);
	memset(cmd_mgr, 0, sizeof(*cmd_mgr));
}

void aicwf_set_cmd_tx(void *dev, struct lmac_msg *msg, uint len)
{
	u8 *buffer = NULL;
	u16 index = 0;
#ifdef AICWF_SDIO_SUPPORT
	struct aic_sdio_dev *sdiodev = (struct aic_sdio_dev *)dev;
	struct aicwf_bus *bus = sdiodev->bus_if;
#else
	struct aic_usb_dev *usbdev = (struct aic_usb_dev *)dev;
	struct aicwf_bus *bus = NULL;
	if (!usbdev->state) {
		printk("down msg \n");
		return;
	}
	bus = usbdev->bus_if;
#endif
	buffer = bus->cmd_buf;

	memset(buffer, 0, CMD_BUF_MAX);
	buffer[0] = (len+4) & 0x00ff;
	buffer[1] = ((len+4) >> 8) &0x0f;
	buffer[2] = 0x11;
	buffer[3] = 0x0;
	index += 4;
	//there is a dummy word
	index += 4;

	//make sure little endian
	put_u16(&buffer[index], msg->id);
	index += 2;
	put_u16(&buffer[index], msg->dest_id);
	index += 2;
	put_u16(&buffer[index], msg->src_id);
	index += 2;
	put_u16(&buffer[index], msg->param_len);
	index += 2;
	memcpy(&buffer[index], (u8 *)msg->param, msg->param_len);

	aicwf_bus_txmsg(bus, buffer, len + 8);
}

